home *** CD-ROM | disk | FTP | other *** search
/ Personal Computer World 2009 February / PCWFEB09.iso / Software / Linux / Kubuntu 8.10 / kubuntu-8.10-desktop-i386.iso / casper / filesystem.squashfs / usr / share / system-config-printer / monitor.py < prev    next >
Text File  |  2008-10-20  |  22KB  |  555 lines

  1. #!/usr/bin/env python
  2.  
  3. ## Copyright (C) 2007, 2008 Tim Waugh <twaugh@redhat.com>
  4. ## Copyright (C) 2007, 2008 Red Hat, Inc.
  5.  
  6. ## This program is free software; you can redistribute it and/or modify
  7. ## it under the terms of the GNU General Public License as published by
  8. ## the Free Software Foundation; either version 2 of the License, or
  9. ## (at your option) any later version.
  10.  
  11. ## This program is distributed in the hope that it will be useful,
  12. ## but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  14. ## GNU General Public License for more details.
  15.  
  16. ## You should have received a copy of the GNU General Public License
  17. ## along with this program; if not, write to the Free Software
  18. ## Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
  19.  
  20. import cups
  21. import dbus
  22. import dbus.glib
  23. import gobject
  24. import time
  25. from debug import *
  26. import pprint
  27.  
  28. global _
  29. _ = lambda x: x
  30. def set_gettext_function (x):
  31.     _ = x
  32. import statereason
  33. from statereason import StateReason
  34. statereason.set_gettext_function (_)
  35.  
  36. CONNECTING_TIMEOUT = 60 # seconds
  37. MIN_REFRESH_INTERVAL = 1 # seconds
  38.  
  39. def state_reason_is_harmless (reason):
  40.     if (reason.startswith ("moving-to-paused") or
  41.         reason.startswith ("paused") or
  42.         reason.startswith ("shutdown") or
  43.         reason.startswith ("stopping") or
  44.         reason.startswith ("stopped-partly")):
  45.         return True
  46.     return False
  47.  
  48. def collect_printer_state_reasons (connection):
  49.     result = {}
  50.     try:
  51.         printers = connection.getPrinters ()
  52.     except cups.IPPError:
  53.         return result
  54.  
  55.     for name, printer in printers.iteritems ():
  56.         reasons = printer["printer-state-reasons"]
  57.         if type (reasons) != list:
  58.             # Work around a bug that was fixed in pycups-1.9.20.
  59.             reasons = [reasons]
  60.         for reason in reasons:
  61.             if reason == "none":
  62.                 break
  63.             if state_reason_is_harmless (reason):
  64.                 continue
  65.             if not result.has_key (name):
  66.                 result[name] = []
  67.             result[name].append (StateReason (name, reason))
  68.     return result
  69.  
  70. class Watcher:
  71.     # Interface definition
  72.     def monitor_exited (self, monitor):
  73.         debugprint (repr (monitor) + " exited")
  74.  
  75.     def state_reason_added (self, monitor, reason):
  76.         debugprint (repr (monitor) + ": +" + repr (reason))
  77.  
  78.     def state_reason_removed (self, monitor, reason):
  79.         debugprint (repr (monitor) + ": -" + repr (reason))
  80.  
  81.     def still_connecting (self, monitor, reason):
  82.         debugprint (repr (monitor) + ": `%s' still connecting" %
  83.                     reason.get_printer ())
  84.  
  85.     def now_connected (self, monitor, printer):
  86.         debugprint (repr (monitor) + ": `%s' now connected" % printer)
  87.  
  88.     def current_printers_and_jobs (self, monitor, printers, jobs):
  89.         debugprint (repr (monitor) + ": printers and jobs lists provided")
  90.  
  91.     def job_added (self, monitor, jobid, eventname, event, jobdata):
  92.         debugprint (repr (monitor) + ": job %d added" % jobid)
  93.  
  94.     def job_event (self, monitor, jobid, eventname, event, jobdata):
  95.         debugprint (repr (monitor) + ": job %d has event `%s'" %
  96.                     (jobid, eventname))
  97.  
  98.     def job_removed (self, monitor, jobid, eventname, event):
  99.         debugprint (repr (monitor) + ": job %d removed" % jobid)
  100.  
  101.     def printer_added (self, monitor, printer):
  102.         debugprint (repr (monitor) + ": printer `%s' added" % printer)
  103.  
  104.     def printer_event (self, monitor, printer, eventname, event):
  105.         debugprint (repr (monitor) + ": printer `%s' has event `%s'" %
  106.                     (printer, eventname))
  107.  
  108.     def printer_removed (self, monitor, printer):
  109.         debugprint (repr (monitor) + ": printer `%s' removed" % printer)
  110.  
  111.     def cups_connection_error (self, monitor):
  112.         debugprint (repr (monitor) + ": CUPS connection error")
  113.  
  114.     def cups_ipp_error (self, monitor, e, m):
  115.         debugprint (repr (monitor) + ": CUPS IPP error (%d, %s)" %
  116.                     (e, repr (m)))
  117.  
  118. class Monitor:
  119.     # Monitor jobs and printers.
  120.     DBUS_PATH="/com/redhat/PrinterSpooler"
  121.     DBUS_IFACE="com.redhat.PrinterSpooler"
  122.  
  123.     def __init__(self, watcher, bus=None, my_jobs=True, specific_dests=None,
  124.                  monitor_jobs=True):
  125.         self.watcher = watcher
  126.         self.my_jobs = my_jobs
  127.         self.specific_dests = specific_dests
  128.         self.monitor_jobs = monitor_jobs
  129.         self.jobs = {}
  130.         self.printer_state_reasons = {}
  131.         self.printers = set()
  132.  
  133.         self.which_jobs = "not-completed"
  134.         self.reasons_seen = {}
  135.         self.connecting_timers = {}
  136.         self.still_connecting = set()
  137.         self.connecting_to_device = {}
  138.         self.received_any_dbus_signals = False
  139.  
  140.         if bus == None:
  141.             bus = dbus.SystemBus ()
  142.  
  143.         bus.add_signal_receiver (self.handle_dbus_signal,
  144.                                  path=self.DBUS_PATH,
  145.                                  dbus_interface=self.DBUS_IFACE)
  146.         self.bus = bus
  147.  
  148.         self.sub_id = -1
  149.         self.refresh ()
  150.  
  151.     def get_jobs (self):
  152.         return self.jobs.copy ()
  153.  
  154.     def cleanup (self):
  155.         if self.sub_id != -1:
  156.             try:
  157.                 c = cups.Connection ()
  158.                 c.cancelSubscription (self.sub_id)
  159.                 debugprint ("Canceled subscription %d" % self.sub_id)
  160.             except:
  161.                 pass
  162.  
  163.         self.bus.remove_signal_receiver (self.handle_dbus_signal,
  164.                                          path=self.DBUS_PATH,
  165.                                          dbus_interface=self.DBUS_IFACE)
  166.  
  167.         self.watcher.monitor_exited (self)
  168.  
  169.     def check_still_connecting(self, printer):
  170.         """Timer callback to check on connecting-to-device reasons."""
  171.         del self.connecting_timers[printer]
  172.         debugprint ("Still-connecting timer fired for `%s'" % printer)
  173.         (printer_jobs, my_printers) = self.sort_jobs_by_printer ()
  174.         self.update_connecting_devices (printer_jobs)
  175.  
  176.         # Don't run this callback again.
  177.         return False
  178.  
  179.     def update_connecting_devices(self, printer_jobs={}):
  180.         """Updates connecting_to_device dict and still_connecting set."""
  181.         time_now = time.time ()
  182.         connecting_to_device = {}
  183.         trouble = False
  184.         for printer, reasons in self.printer_state_reasons.iteritems ():
  185.             connected = True
  186.             for reason in reasons:
  187.                 if reason.get_reason () == "connecting-to-device":
  188.                     have_processing_job = False
  189.                     for job, data in \
  190.                             printer_jobs.get (printer, {}).iteritems ():
  191.                         state = data.get ('job-state',
  192.                                           cups.IPP_JOB_CANCELED)
  193.                         if state == cups.IPP_JOB_PROCESSING:
  194.                             have_processing_job = True
  195.                             break
  196.  
  197.                     if not have_processing_job:
  198.                         debugprint ("Ignoring stale connecting-to-device x")
  199.                         continue
  200.  
  201.                     # Build a new connecting_to_device dict.  If our existing
  202.                     # dict already has an entry for this printer, use that.
  203.                     printer = reason.get_printer ()
  204.                     t = self.connecting_to_device.get (printer, time_now)
  205.                     connecting_to_device[printer] = t
  206.                     debugprint ("Connecting time: %d" % (time_now - t))
  207.                     if time_now - t >= CONNECTING_TIMEOUT:
  208.                         if have_processing_job:
  209.                             self.still_connecting.add (printer)
  210.                             self.watcher.still_connecting (self, reason)
  211.                             if self.connecting_timers.has_key (printer):
  212.                                 gobject.source_remove (self.connecting_timers
  213.                                                        [printer])
  214.                                 del self.connecting_timers[printer]
  215.                                 debugprint ("Stopped connecting timer "
  216.                                             "for `%s'" % printer)
  217.  
  218.                     connected = False
  219.                     break
  220.  
  221.             if connected and self.connecting_timers.has_key (printer):
  222.                 gobject.source_remove (self.connecting_timers[printer])
  223.                 del self.connecting_timers[printer]
  224.                 debugprint ("Stopped connecting timer for `%s'" % printer)
  225.  
  226.         # Clear any previously-notified errors that are now fine.
  227.         remove = set()
  228.         for printer in self.still_connecting:
  229.             if not connecting_to_device.has_key (printer):
  230.                 remove.add (printer)
  231.                 self.watcher.now_connected (self, printer)
  232.                 if self.connecting_timers.has_key (printer):
  233.                     gobject.source_remove (self.connecting_timers[printer])
  234.                     del self.connecting_timers[printer]
  235.                     debugprint ("Stopped connecting timer for `%s'" % printer)
  236.  
  237.         self.still_connecting = self.still_connecting.difference (remove)
  238.         self.connecting_to_device = connecting_to_device
  239.  
  240.     def check_state_reasons(self, my_printers=set(), printer_jobs={}):
  241.         # Look for any new reasons since we last checked.
  242.         old_reasons_seen_keys = self.reasons_seen.keys ()
  243.         reasons_now = set()
  244.         for printer, reasons in self.printer_state_reasons.iteritems ():
  245.             for reason in reasons:
  246.                 tuple = reason.get_tuple ()
  247.                 printer = reason.get_printer ()
  248.                 reasons_now.add (tuple)
  249.                 if not self.reasons_seen.has_key (tuple):
  250.                     # New reason.
  251.                     self.watcher.state_reason_added (self, reason)
  252.                     self.reasons_seen[tuple] = reason
  253.  
  254.                 if (reason.get_reason () == "connecting-to-device" and
  255.                     not self.connecting_to_device.has_key (printer)):
  256.                     # First time we've seen this.
  257.  
  258.                     have_processing_job = False
  259.                     for job, data in \
  260.                             printer_jobs.get (printer, {}).iteritems ():
  261.                         state = data.get ('job-state',
  262.                                           cups.IPP_JOB_CANCELED)
  263.                         if state == cups.IPP_JOB_PROCESSING:
  264.                             have_processing_job = True
  265.                             break
  266.  
  267.                     if have_processing_job:
  268.                         t = gobject.timeout_add ((1 + CONNECTING_TIMEOUT)
  269.                                                  * 1000,
  270.                                                  self.check_still_connecting,
  271.                                                  printer)
  272.                         self.connecting_timers[printer] = t
  273.                         debugprint ("Start connecting timer for `%s'" %
  274.                                     printer)
  275.                     else:
  276.                         # Don't notify about this, as it must be stale.
  277.                         debugprint ("Ignoring stale connecting-to-device")
  278.                         debugprint (pprint.pformat (printer_jobs))
  279.  
  280.         self.update_connecting_devices (printer_jobs)
  281.         items = self.reasons_seen.keys ()
  282.         for tuple in items:
  283.             if not tuple in reasons_now:
  284.                 # Reason no longer present.
  285.                 reason = self.reasons_seen[tuple]
  286.                 del self.reasons_seen[tuple]
  287.                 self.watcher.state_reason_removed (self, reason)
  288.  
  289.     def get_notifications(self):
  290.         debugprint ("get_notifications")
  291.         try:
  292.             c = cups.Connection ()
  293.  
  294.             try:
  295.                 try:
  296.                     notifications = c.getNotifications ([self.sub_id],
  297.                                                         [self.sub_seq + 1])
  298.                 except AttributeError:
  299.                     notifications = c.getNotifications ([self.sub_id])
  300.             except cups.IPPError, (e, m):
  301.                 if e == cups.IPP_NOT_FOUND:
  302.                     # Subscription lease has expired.
  303.                     self.sub_id = -1
  304.                     self.refresh ()
  305.                     return False
  306.  
  307.                 self.watcher.cups_ipp_error (self, e, m)
  308.                 return True
  309.         except RuntimeError:
  310.             self.watcher.cups_connection_error (self)
  311.             return True
  312.  
  313.         deferred_calls = []
  314.         jobs = self.jobs.copy ()
  315.         for event in notifications['events']:
  316.             seq = event['notify-sequence-number']
  317.             try:
  318.                 if seq <= self.sub_seq:
  319.                     # Work around a bug in pycups < 1.9.34
  320.                     continue
  321.             except AttributeError:
  322.                 pass
  323.             self.sub_seq = seq
  324.             nse = event['notify-subscribed-event']
  325.             debugprint ("%d %s %s" % (seq, nse, event['notify-text']))
  326.             debugprint (pprint.pformat (event))
  327.             if nse.startswith ('printer-'):
  328.                 # Printer events
  329.                 name = event['printer-name']
  330.                 if nse == 'printer-added' and name not in self.printers:
  331.                     self.printers.add (name)
  332.                     deferred_calls.append ((self.watcher.printer_added,
  333.                                             (self, name)))
  334.  
  335.                 elif nse == 'printer-deleted' and name in self.printers:
  336.                     self.printers.remove (name)
  337.                     items = self.reasons_seen.keys ()
  338.                     for tuple in items:
  339.                         if tuple[1] == name:
  340.                             reason = self.reasons_seen[tuple]
  341.                             del self.reasons_seen[tuple]
  342.                             deferred_calls.append ((self.watcher.state_reason_removed,
  343.                                                     (self, reason)))
  344.                             
  345.                     if self.printer_state_reasons.has_key (name):
  346.                         del self.printer_state_reasons[name]
  347.  
  348.                     deferred_calls.append ((self.watcher.printer_removed,
  349.                                             (self, name)))
  350.                 elif name in self.printers:
  351.                     printer_state_reasons = event['printer-state-reasons']
  352.                     if type (printer_state_reasons) != list:
  353.                         # Work around a bug in pycups < 1.9.36
  354.                         printer_state_reasons = [printer_state_reasons]
  355.  
  356.                     reasons = []
  357.                     for reason in printer_state_reasons:
  358.                         if reason == "none":
  359.                             break
  360.                         if state_reason_is_harmless (reason):
  361.                             continue
  362.                         reasons.append (StateReason (name, reason))
  363.                     self.printer_state_reasons[name] = reasons
  364.  
  365.                     deferred_calls.append ((self.watcher.printer_event,
  366.                                             (self, name, nse, event)))
  367.                 continue
  368.  
  369.             # Job events
  370.             jobid = event['notify-job-id']
  371.             if (nse == 'job-created' or
  372.                 (nse == 'job-state-changed' and
  373.                  not jobs.has_key (jobid) and
  374.                  event['job-state'] == cups.IPP_JOB_PROCESSING)):
  375.                 if (self.specific_dests != None and
  376.                     event['printer-name'] not in self.specific_dests):
  377.                     continue
  378.  
  379.                 try:
  380.                     attrs = c.getJobAttributes (jobid)
  381.                     if (self.my_jobs and
  382.                         attrs['job-originating-user-name'] != cups.getUser ()):
  383.                         continue
  384.  
  385.                     jobs[jobid] = attrs
  386.                 except AttributeError:
  387.                     jobs[jobid] = {'job-k-octets': 0}
  388.                 except cups.IPPError, (e, m):
  389.                     self.watcher.cups_ipp_error (self, e, m)
  390.                     jobs[jobid] = {'job-k-octets': 0}
  391.  
  392.                 deferred_calls.append ((self.watcher.job_added,
  393.                                         (self, jobid, nse, event,
  394.                                          jobs[jobid].copy ())))
  395.             elif nse == 'job-completed':
  396.                 try:
  397.                     del jobs[jobid]
  398.                     deferred_calls.append ((self.watcher.job_removed,
  399.                                             (self, jobid, nse, event)))
  400.                 except KeyError:
  401.                     pass
  402.                 continue
  403.  
  404.             try:
  405.                 job = jobs[jobid]
  406.             except KeyError:
  407.                 continue
  408.  
  409.             for attribute in ['job-state',
  410.                               'job-name']:
  411.                 job[attribute] = event[attribute]
  412.             if event.has_key ('notify-printer-uri'):
  413.                 job['job-printer-uri'] = event['notify-printer-uri']
  414.  
  415.             deferred_calls.append ((self.watcher.job_event,
  416.                                    (self, jobid, nse, event, job.copy ())))
  417.  
  418.         self.update (jobs)
  419.         self.jobs = jobs
  420.  
  421.         for (fn, args) in deferred_calls:
  422.             fn (*args)
  423.  
  424.         # Update again when we're told to.  If we're getting CUPS
  425.         # D-Bus signals, however, rely on those instead.
  426.         if not self.received_any_dbus_signals:
  427.             gobject.source_remove (self.update_timer)
  428.             interval = 1000 * notifications['notify-get-interval']
  429.             self.update_timer = gobject.timeout_add (interval,
  430.                                                      self.get_notifications)
  431.  
  432.         return False
  433.  
  434.     def refresh(self):
  435.         debugprint ("refresh")
  436.  
  437.         try:
  438.             c = cups.Connection ()
  439.         except RuntimeError:
  440.             self.watcher.cups_connection_error (self)
  441.             return
  442.  
  443.         if self.sub_id != -1:
  444.             try:
  445.                 c.cancelSubscription (self.sub_id)
  446.             except cups.IPPError, (e, m):
  447.                 self.watcher.cups_ipp_error (self, e, m)
  448.  
  449.             gobject.source_remove (self.update_timer)
  450.             debugprint ("Canceled subscription %d" % self.sub_id)
  451.  
  452.         try:
  453.             del self.sub_seq
  454.         except AttributeError:
  455.             pass
  456.  
  457.         events = ["printer-added",
  458.                   "printer-deleted",
  459.                   "printer-state-changed"]
  460.         if self.monitor_jobs:
  461.             events.extend (["job-created",
  462.                             "job-completed",
  463.                             "job-stopped",
  464.                             "job-progress",
  465.                             "job-state-changed"])
  466.  
  467.         try:
  468.             self.sub_id = c.createSubscription ("/", events=events)
  469.         except cups.IPPError, (e, m):
  470.             self.watcher.cups_ipp_error (self, e, m)
  471.  
  472.         self.update_timer = gobject.timeout_add (MIN_REFRESH_INTERVAL * 1000,
  473.                                                  self.get_notifications)
  474.         debugprint ("Created subscription %d" % self.sub_id)
  475.  
  476.         try:
  477.             if self.monitor_jobs:
  478.                 jobs = c.getJobs (which_jobs=self.which_jobs,
  479.                                   my_jobs=self.my_jobs)
  480.             else:
  481.                 jobs = {}
  482.             self.printer_state_reasons = collect_printer_state_reasons (c)
  483.             dests = c.getDests ()
  484.             printers = set()
  485.             for (printer, instance) in dests.keys ():
  486.                 if printer == None:
  487.                     continue
  488.                 if instance != None:
  489.                     continue
  490.                 printers.add (printer)
  491.             self.printers = printers
  492.         except cups.IPPError, (e, m):
  493.             self.watcher.cups_ipp_error (self, e, m)
  494.             return
  495.         except RuntimeError:
  496.             self.watcher.cups_connection_error (self)
  497.             return
  498.  
  499.         if self.specific_dests != None:
  500.             for jobid in jobs.keys ():
  501.                 uri = jobs[jobid].get('job-printer-uri', '/')
  502.                 i = uri.rfind ('/')
  503.                 printer = uri[i + 1:]
  504.                 if printer not in self.specific_dests:
  505.                     del jobs[jobid]
  506.  
  507.         self.watcher.current_printers_and_jobs (self, self.printers.copy (),
  508.                                                 jobs.copy ())
  509.         self.update (jobs)
  510.  
  511.         self.jobs = jobs
  512.         return False
  513.  
  514.     def sort_jobs_by_printer (self, jobs=None):
  515.         if jobs == None:
  516.             jobs = self.jobs
  517.  
  518.         my_printers = set()
  519.         printer_jobs = {}
  520.         for job, data in jobs.iteritems ():
  521.             state = data.get ('job-state', cups.IPP_JOB_CANCELED)
  522.             if state >= cups.IPP_JOB_CANCELED:
  523.                 continue
  524.             uri = data.get ('job-printer-uri', '')
  525.             i = uri.rfind ('/')
  526.             if i == -1:
  527.                 continue
  528.             printer = uri[i + 1:]
  529.             my_printers.add (printer)
  530.             if not printer_jobs.has_key (printer):
  531.                 printer_jobs[printer] = {}
  532.             printer_jobs[printer][job] = data
  533.  
  534.         return (printer_jobs, my_printers)
  535.  
  536.     def update(self, jobs):
  537.         debugprint ("update")
  538.         (printer_jobs, my_printers) = self.sort_jobs_by_printer (jobs)
  539.         self.check_state_reasons (my_printers, printer_jobs)
  540.  
  541.     def handle_dbus_signal(self, *args):
  542.         gobject.source_remove (self.update_timer)
  543.         self.update_timer = gobject.timeout_add (200, self.get_notifications)
  544.         if not self.received_any_dbus_signals:
  545.             self.received_any_dbus_signals = True
  546.  
  547. if __name__ == '__main__':
  548.     set_debugging (True)
  549.     m = Monitor (Watcher ())
  550.     loop = gobject.MainLoop ()
  551.     try:
  552.         loop.run ()
  553.     finally:
  554.         m.cleanup ()
  555.